/*-------------------------------------------------------------------------
   stop.c
   Copyright (C) 2011 One Laptop per Child

   This program is free software; you can redistribute it and/or modify it
   under the terms of the GNU General Public License as published by the
   Free Software Foundation; either version 2, or (at your option) any
   later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

   In other words, you are welcome to use, share and improve this program.
   You are forbidden to forbid anyone else to use, share and improve
   what you give them.   Help stamp out software-hoarding!

   As a special exception, you may use this file as part of a free software
   library for the XO of the One Laptop per Child project without restriction.
   Specifically, if other files instantiate
   templates or use macros or inline functions from this file, or you compile
   this file and link it with other files to produce an executable, this
   file does not by itself cause the resulting executable to be covered by
   the GNU General Public License.  This exception does not however
   invalidate any other reasons why the executable file might be covered by
   the GNU General Public License.
-------------------------------------------------------------------------*/

#include <stdio.h>
#include "cc_compat.h"
#include "chip.h"
#include "io.h"
#include "uart.h"
#include "timer.h"
#include "battery.h"
#include "adc.h"
#include "ac.h"
#include "power.h"
#include "ds2756.h"
#include "onewirebatt.h"
#include "every.h"


static unsigned char xdata autostop_enabled;
static unsigned char xdata gpwu_save[4 * 8];

#define DEBUG_STOP

#ifdef DEBUG_STOP
void dump_gpwu(char before)
{
	unsigned int i,dval;

	if (before)
		puts("before:");
	else
		puts("after:");

	for (i = 0; i < 8; i++)
	{
		if (before && gpwu_save[i] != 0)
		{
			printf("savedWUEN[%d] = %x\n",(uint)i,(uint)gpwu_save[i]);
		}
		dval = gpwu[GPWUPF00 + i];
		if ( dval != 0)
		{
			printf("WUPF[%d] = %x\n",i,dval);
		}
		dval = gpwu[GPWUEN00 + i];
		if ( dval != 0)
		{
			printf("WUEN[%d] = %x\n",i,dval);
		}
	}
}
#else
#define dump_gpwu(x)
#endif

static void save_gpwu_regs(void)
{
	char i, j;
	j = 0;
	for (i = 0; i < 8; i++, j++)
		gpwu_save[j] = gpwu[GPWUEN00 + i];

	for (i = 0; i < 8; i++, j++)
		gpwu_save[j] = gpwu[GPWUPS00 + i];

	for (i = 0; i < 8; i++, j++)
		gpwu_save[j] = gpwu[GPWUEL00 + i];

	for (i = 0; i < 8; i++, j++)
		gpwu_save[j] = gpwu[GPWUCHG00 + i];

	// the SOC_SLEEP interrupt (GPIO_0B) is the only one we have
	// on P3IE1.  we need to prevent the handler being called
	// from STOP mode, because if it runs, and clears the source,
	// we'll return from it and stay in STOP mode.  so disable
	// the interupt at the 8051 level, for now.  there's probably
	// a better way.
	P3IE1 = 0;  // HACK -- fixme.
}

static void restore_gpwu_regs(void)
{
	char i, j;

	j = 0;
	for (i = 0; i < 8; i++, j++)
		gpwu[GPWUEN00 + i] = gpwu_save[j];

	for (i = 0; i < 8; i++, j++)
		gpwu[GPWUPS00 + i] = gpwu_save[j];

	for (i = 0; i < 8; i++, j++)
		gpwu[GPWUEL00 + i] = gpwu_save[j];

	for (i = 0; i < 8; i++, j++)
		gpwu[GPWUCHG00 + i] = gpwu_save[j];

	P3IE1 = 1;  // HACK -- fixme.
}

static void set_wakeups(unsigned char in_suspend)
{
	char i;

	for (i = 0; i < 8; i++)
	{
		gpwu[GPWUEN00 + i] = 0;
		gpwu[GPWUPF00 + i] = 0xff;
	}

	if (in_suspend)
	{
		if ( board_rev > 0xb3 || board_rev == 0xa1 )
		{
			/* configure wakeups from the sleep indicator,
			 * so we'll wake immediately when the SoC wakes up.  */
			IO_WAKEUP_POLARITY_LOW(GPIO_SOC_SLEEP);
			IO_WAKEUP_EDGE(GPIO_SOC_SLEEP);
			IO_WAKEUP_PENDING_CLEAR(GPIO_SOC_SLEEP);
			IO_WAKEUP_ENABLE(GPIO_SOC_SLEEP);
		}
		else
		{
			/* no SOC_SLEEP line on early board, but we need to
			 * wake on something.  configure wakeup from sdi CMD
			 * being raised -- we already ask the kernel for a
			 * command hint on those boards for on s/r, so that
			 * command will wake us.  */
			IO_WAKEUP_POLARITY_HIGH(GPIO_EC_SPI_CMD);
			IO_WAKEUP_EDGE(GPIO_EC_SPI_CMD);
			IO_WAKEUP_PENDING_CLEAR(GPIO_EC_SPI_CMD);
			IO_WAKEUP_ENABLE(GPIO_EC_SPI_CMD);
		}

#ifdef NO_PS2
		/* configure to be woken if the keyboard or touchpad clock
		 * line drops
		 */
		IO_WAKEUP_POLARITY_LOW(GPIO_EC_TPD_CLK);
		IO_WAKEUP_EDGE(GPIO_EC_TPD_CLK);
		IO_WAKEUP_PENDING_CLEAR(GPIO_EC_TPD_CLK);
		IO_WAKEUP_ENABLE(GPIO_EC_TPD_CLK);

		IO_WAKEUP_POLARITY_LOW(GPIO_EC_KBD_CLK);
		IO_WAKEUP_EDGE(GPIO_EC_KBD_CLK);
		IO_WAKEUP_PENDING_CLEAR(GPIO_EC_KBD_CLK);
		IO_WAKEUP_ENABLE(GPIO_EC_KBD_CLK);
#endif

	}

	/* configure wakeup from power button press */
	IO_WAKEUP_POLARITY_LOW(GPIO_PWR_BTN);
	IO_WAKEUP_EDGE(GPIO_PWR_BTN);
	IO_WAKEUP_PENDING_CLEAR(GPIO_PWR_BTN);
	IO_WAKEUP_ENABLE(GPIO_PWR_BTN);

	/* ac insertion or removal */
	// broken: IO_WAKEUP_TOGGLE_ENABLE(GPIO_AC_OK);
	// the "either edge" mode seems to continually trigger,
	// so we use separate edge interrupts instead:
	if (IO_ACTIVE_HIGH(GPIO_CHRG_AC_OK))
		IO_WAKEUP_POLARITY_LOW(GPIO_CHRG_AC_OK);
	else
		IO_WAKEUP_POLARITY_HIGH(GPIO_CHRG_AC_OK);
	IO_WAKEUP_EDGE(GPIO_CHRG_AC_OK);
	IO_WAKEUP_PENDING_CLEAR(GPIO_CHRG_AC_OK);
	IO_WAKEUP_ENABLE(GPIO_CHRG_AC_OK);

	/* configure the EDI CS input to wake us, so we have a
	 * better chance of programming the EC when in STOP mode
	 */
	IO_WAKEUP_POLARITY_LOW(GPIO_EDI_CS);
	IO_WAKEUP_EDGE(GPIO_EDI_CS);
	IO_WAKEUP_PENDING_CLEAR(GPIO_EDI_CS);
	IO_WAKEUP_ENABLE(GPIO_EDI_CS);

#ifdef SERIAL_RX_WAKEUP // we don't sleep when power is off if we do this
	/* configure the serial RX line to wake us, for convenience */
	IO_WAKEUP_POLARITY_LOW(GPIO_EC_RX);
	IO_WAKEUP_EDGE(GPIO_EC_RX);
	IO_WAKEUP_PENDING_CLEAR(GPIO_EC_RX);
	IO_WAKEUP_ENABLE(GPIO_EC_RX);
#endif
}

static char check_wakeups(void)
{
	char i;

	for (i = 0; i < 8; i++)
	{
		if (gpwu[GPWUPF00 + i] & gpwu[GPWUEN00 + i])
		{
			// printf("chk: WUPF[%d] = %x\n",(uint)i,(uint)gpwu[GPWUPF00 + i]);
			return 1;
		}
	}
	return 0;
}

/* these disables/reenables could clearly be combined into fewer
 * register writes if needed.
 */
static void disable_output_drivers(void)
{
	output_disable_EN_MAIN_PWR();
	output_disable_EN_VCORE_PWR();
	output_disable_EN_1_8V_PMIC();
	output_disable_EN_1_8V_GPIO();
	output_disable_EN_3_3V_SOC();
	output_disable_EN_3_3V_NAND();
	output_disable_EN_DCON_PWR();
	output_disable_EC_IRQ();
	output_disable_EN_KBD_PWR();
	output_disable_EN_1_5V_DDR3();
	output_disable_EN_1_2V();
	output_disable_EN_USB_PWR();
	output_disable_EN_SD1_PWR();
	output_disable_EN_SD2_PWR();
	output_disable_LED_CHG_GREEN();
	output_disable_LED_CHG_RED();

	adc_deinit();
}

static void reenable_output_drivers(void)
{
	output_enable_EN_MAIN_PWR();
	output_enable_EN_VCORE_PWR();
	output_enable_EN_1_8V_PMIC();
	output_enable_EN_1_8V_GPIO();
	output_enable_EN_3_3V_SOC();
	output_enable_EN_3_3V_NAND();
	output_enable_EC_IRQ();
	output_enable_EN_DCON_PWR();
	output_enable_EN_KBD_PWR();
	output_enable_EN_1_5V_DDR3();
	output_enable_EN_1_2V();
	output_enable_EN_USB_PWR();
	output_enable_EN_SD1_PWR();
	output_enable_EN_SD2_PWR();
	output_enable_LED_CHG_GREEN();
	output_enable_LED_CHG_RED();

	adc_init();
}

void enable_stop_mode(char e)
{
	autostop_enabled = e;
}

int stop_mode_timer;

/* return value is whether we entered stop mode, or were eligible to */
unsigned char enter_stop_mode(unsigned char autostop, unsigned char in_suspend)
{
	unsigned int wdog_time;
	unsigned char wdtpf;
	signed int  ACR;

	/* it can be useful to prevent automatic stops, while still
	 * permitting manual stops via the monitor.
	 */
	if (autostop)
	{
		if (is_ac_adapter_in() ||
				timed_host_wakeup_active() ||
				!autostop_enabled ||
				(battery_debug_flags & BATTERY_DEBUG ))
		{
			stop_mode_timer = 0;
			return 0;
		}
	}

	/* we delay briefly so that the EC can gather a min power
	 * measurement before stopping.
	 */
	if (stop_mode_timer == 0)
	{
		stop_mode_timer = get_ms_timer();
	}

	if (in_suspend)
	{
		if (!check_timer(stop_mode_timer, 600) )
		{
			return 1;
		}
	}
	else
	{
		if (!check_timer(stop_mode_timer, 3000) )
		{
			return 1;
		}
	}

	if (onewire_operation_pending())
	{
		return 1;
	}

	stop_mode_timer = 0;

	ACR = GAUGE_CACHE[DS_ACR_MSB];
	ACR = (ACR  << 8) + GAUGE_CACHE[DS_ACR_LSB];

	print_tstamp();
	// ACR dump is usfull for checking long term suspend power draw
	printf("...zzzzz  ACR:%d\n", ACR);

	if (!in_suspend)
		disable_output_drivers();

	save_gpwu_regs();
	set_wakeups(in_suspend);

	dump_gpwu(1);

	tx_drain();

	if (in_suspend)
	{
		// host is still on, blink the suspend LED

		while (!check_wakeups())
		{
			ms_delay_block(5);
			if (read_LED_PWR())
			{
				set_LED_PWR_off();
				wdog_time = 4000;  // 4 secs off
			}
			else
			{
				set_LED_PWR_on();
				wdog_time = 100;  // .1 secs on (wdog min is really .125)
			}

			watchdog_start(wdog_time);

			PMUCFG = 0x86;	/* enter STOP mode, wake with GPWU or watchdog */
			ACC = PCON;
			ACC = PCON;
			ACC = PCON;		/* magic delay from vendor */

			wdtpf = WDTPF;
			watchdog_stop();
		}

		set_LED_PWR_on();
	}
	else
	{
		// SoC is powered off
		PMUCFG = 0x86;	/* enter STOP mode, wake with GPWU or watchdog */
		ACC = PCON;
		ACC = PCON;
		ACC = PCON;		/* magic delay from vendor */

	}

	print_tstamp();
	puts("Brrrrinnnggg!!");

	dump_gpwu(0);

	restore_gpwu_regs();
	if (!in_suspend)
		reenable_output_drivers();

	return 1;
}

